Unsetting optional offset might still give a list.#5029
Unsetting optional offset might still give a list.#5029ondrejmirtes merged 10 commits intophpstan:2.1.xfrom
Conversation
…ing keys When `ConstantArrayType::unsetOffset()` removed an optional trailing key from a list type, it unconditionally set `isList` to `No`. This caused `TypeCombinator::remove()` to produce `NeverType` when re-intersecting with `AccessoryArrayListType`, since the list accessor rejects non-list arrays. The `NeverType` falsy branch then collapsed scope merging after conditionals like ternaries, propagating the narrowed (truthy) type beyond the conditional and triggering false "always true" reports on subsequent `array_key_exists()` calls. The fix preserves the original `isList` value when unsetting an optional key whose removal leaves the remaining keys as consecutive integers starting from 0. Fixes phpstan/phpstan#14177
| return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, TrinaryLogic::createNo()); | ||
| $newIsList = TrinaryLogic::createNo(); | ||
| if (!$this->isList->no() && in_array($i, $this->optionalKeys, true)) { | ||
| $newIsList = TrinaryLogic::createMaybe(); |
There was a problem hiding this comment.
Not always, right? The new type is a list only if we're removing an optional key and:
- The key is the last one
- Or all of the keys after it are also optional
If you're going to implement this logic, add failing tests first 😊
There was a problem hiding this comment.
Per phpstan/phpstan#12768 even unsetting the last item makes it no longer a list because of internal pointer and append with [] will create a hole
There was a problem hiding this comment.
Yeah but the point is that we're unsetting something that might not be on the array. So it might still be a list.
d2ef261 to
598eaaa
Compare
598eaaa to
89f5a54
Compare
59b5366 to
405eecf
Compare
f76e22c to
faf726d
Compare
|
This pull request has been marked as ready for review. |
| * So the nextAutoIndexes won't change, and the array might still be a list even with PHPStan definition. | ||
| * | ||
| * @param list<ConstantIntegerType|ConstantStringType> $newKeyTypes | ||
| * @param int[] $newOptionalKeys |
There was a problem hiding this comment.
I don't like these alignments. There should be just a single space between the type and variable.
| { | ||
| assertType('true', array_is_list($b)); | ||
| unset($b[1]); | ||
| assertType('false', array_is_list($b)); // Could be true |
There was a problem hiding this comment.
It could not. When you're unsetting an existing key, adding a new one will create a gap.
There was a problem hiding this comment.
Cf https://3v4l.org/4GBOm#v8.3.30
It's still a list, and it change when we add back a new element
So technically it could be true, and PHPStan should be aware it's false as soon as we add back an element.
But I agree it's harder.
I removed the comments.
There was a problem hiding this comment.
The problem is that the logic is unsound when passing the array around to function arguments.
When we accept a list, it must mean that after appending a new item, it's still a list.
There was a problem hiding this comment.
Yes. Maybe the right fix will be to introduce a DynamicReturnTypeExtension for array_is_list to handle this special case.
Anyway, this PR should be ok to fix the issue 14177 so far.
| * @param int[] $newOptionalKeys | ||
| * @param int[] $newOptionalKeys | ||
| */ | ||
| private static function isListAfterUnset(array $newKeyTypes, array $newOptionalKeys, TrinaryLogic $arrayIsList, bool $unsetOptionalKey): TrinaryLogic |
There was a problem hiding this comment.
you might be able to kill the mutation with somethig like
There was a problem hiding this comment.
I don't think so, it will be an union of two ConstantArrayType ; and each one will be handled separatly by this method.
Also, the tests are failing locally with the mutation ; dunno why it's reported as not caught then.
|
Thank you! |
When doing
array_key_existsonlist{0: string, 1: string, 2?: string, 3?: string},the ArrayKeyExistsFunctionTypeSpecifyingExtension is specify
HasOffsetTypeand in the else condition we're callingtryRemoveon thisHasOffsetType.ConstantArrayType::tryRemove do
but the new array created is always with
$this->isList = Ternary::no.The issue is that the intersection with List gives Never ; see
https://phpstan.org/r/c8177c3c-a643-43dc-b62f-b207740d75f8
Technically,
This is fully related to phpstan/phpstan#11628
see https://phpstan.org/r/fb18be32-9fba-42ba-8815-b767734cac6a
This is kinda tricky as changing
$newIsList = TrinaryLogic::createMaybe()to maybe or true will then creates an issue if I append an element later (cause PHPStan won't detect it's not a list anymore).Also, some extra thought: when we're inside the
elsepart of an array_key_exists call we're not really calling unset on the array. So maybe a simpler way to solve this, is tounset$newIsListor preserve listCloses phpstan/phpstan#14177